use std::path::Path;

use serde::{Deserialize, Serialize};
use tracing::{debug, error, trace};

use bc_base::{
    PrivateKeyBytes, PublicKeyBytes, PublicKeyHashBytes, TransactionId,
    wallet::wallet_cache::WalletCache,
};
use bc_db::{BaseBcDb, DBError};

#[cfg(debug_assertions)]
use bc_base::wallet::ItemNumberInWalletDb;

#[cfg(debug_assertions)]
use bc_db::DBIteratorMode;

#[derive(Debug, Deserialize, Serialize)]
pub enum WalletDbKey {
    AccountKeyPair(PublicKeyBytes),
    AccountName(PublicKeyHashBytes),
    DefaultKey, // PublicKeyHashBytes
    WalletTx(TransactionId),
}

#[derive(Debug)]
pub struct WalletStorageBase {
    wallet_db: BaseBcDb,
}

// base interface
impl WalletStorageBase {
    /// Create a new instance of Storage with the given path
    pub fn new(path: &Path) -> Result<Self, DBError> {
        Ok(Self {
            wallet_db: BaseBcDb::new(path)?,
        })
    }

    #[cfg(test)]
    pub fn destroy_db_in_disk(path: &Path) {
        BaseBcDb::destroy_db_in_disk(path);
    }

    pub async fn flush_database(&self) -> Result<(), DBError> {
        debug!("Flushing database...");
        self.wallet_db.flush().await?;
        Ok(())
    }

    pub async fn put(&self, key: WalletDbKey, value: Vec<u8>) -> Result<(), DBError> {
        let k = Self::serialize_key(&key);
        self.wallet_db.put(&k, value).await?;
        Ok(())
    }

    pub async fn get(&self, key: WalletDbKey) -> Result<Option<Vec<u8>>, DBError> {
        let k = Self::serialize_key(&key);
        let t = self.wallet_db.get(&k).await?;
        if let Some(v) = t {
            Ok(Some(v))
        } else {
            Ok(None)
        }
    }

    pub async fn erase(&self, key: WalletDbKey) -> Result<(), DBError> {
        let k = Self::serialize_key(&key);
        self.wallet_db.erase(&k).await?;
        Ok(())
    }

    // pub async fn exists(&self, key: WalletDbKey) -> bool {
    //     let k = Self::serialize_key(&key);
    //     self.wallet_db.exists(&k).await
    // }

    #[cfg(debug_assertions)]
    pub async fn get_item_number(&self) -> ItemNumberInWalletDb {
        let db = self.wallet_db.db.clone();
        let result = tokio::task::spawn_blocking(move || {
            let iter = db.iterator(DBIteratorMode::Start);
            let mut item_number = ItemNumberInWalletDb::new();
            for it in iter {
                let Ok((key, _value)) = it else {
                    error!("Failed to iterator wallet db.");
                    continue;
                };
                let key = String::from_utf8(key.to_vec()).unwrap();
                let key = Self::deserialize_key(&key); //
                match key {
                    WalletDbKey::AccountKeyPair(_pub_key) => {
                        item_number.account_key_pair_number += 1;
                    }
                    WalletDbKey::WalletTx(_tx_id) => {
                        item_number.wallet_tx_number += 1;
                    }
                    WalletDbKey::AccountName(_pub_key_hash) => {
                        item_number.account_name_number += 1;
                    }
                    WalletDbKey::DefaultKey => {
                        item_number.default_key_number += 1;
                    }
                }
            }

            item_number
        })
        .await;

        match result {
            Ok(item_number) => item_number,
            Err(e) => {
                error!("Failed to get item number in wallet db: {}", e);
                ItemNumberInWalletDb::new()
            }
        }
    }

    fn serialize_key(key: &WalletDbKey) -> String {
        hex::encode(bincode::serialize(key).unwrap())
    }

    fn deserialize_key(key: &str) -> WalletDbKey {
        let k = hex::decode(key).unwrap();
        bincode::deserialize(&k).unwrap()
    }
}

// load / store wallet
impl WalletStorageBase {
    pub async fn load_to_wallet_cache(&self, mut wallet_cache: WalletCache) -> WalletCache {
        let db = self.wallet_db.db.clone();
        let result = tokio::task::spawn_blocking(move || {
            for it in db.iterator(bc_db::DBIteratorMode::Start) {
                if let Err(e) = it {
                    error!("Error: {:?}", e);
                    return wallet_cache;
                }

                let (key, value) = it.unwrap();
                let key = String::from_utf8(key.to_vec()).unwrap();
                let key = Self::deserialize_key(&key); //
                trace!("key: {:?}\nvalue: {:?}", key, value);

                match key {
                    WalletDbKey::AccountKeyPair(pub_key) => {
                        let priv_key: PrivateKeyBytes = bincode::deserialize(&value).unwrap();
                        wallet_cache.add_keypair(&pub_key, &priv_key).unwrap();
                    }
                    WalletDbKey::AccountName(pub_key_hash) => {
                        let name = String::from_utf8(value.to_vec()).unwrap();
                        wallet_cache.add_account_name(&pub_key_hash, &name).unwrap();
                    }
                    WalletDbKey::DefaultKey => {
                        let pub_key_hash: PublicKeyHashBytes =
                            bincode::deserialize(&value).unwrap();
                        wallet_cache.set_default_account_(&pub_key_hash).unwrap();
                    }
                    WalletDbKey::WalletTx(tx_id) => {
                        let tx = bincode::deserialize(&value).unwrap();
                        wallet_cache.add_wallet_tx(&tx_id, tx);
                    }
                }
            }

            wallet_cache
        })
        .await;

        result.unwrap()
    }

    pub async fn store_wallet(&self, wallet_cache: &WalletCache) -> Result<(), DBError> {
        debug!("Storing wallet...");
        for (pub_key, priv_key) in wallet_cache.get_keypairs() {
            let key = WalletDbKey::AccountKeyPair(pub_key.clone());
            let value = bincode::serialize(&priv_key).unwrap();
            self.put(key, value).await?;
        }

        for (pub_key_hash, name) in wallet_cache.get_account_names() {
            let key = WalletDbKey::AccountName(*pub_key_hash);
            let value = name.as_bytes().to_vec();
            self.put(key, value).await?;
        }

        if let Some(pub_key_hash) = wallet_cache.get_default_account() {
            let key = WalletDbKey::DefaultKey;
            let value = bincode::serialize(&pub_key_hash).unwrap();
            self.put(key, value).await?;
        }

        for (tx_id, tx) in wallet_cache.get_wallet_txs() {
            let key = WalletDbKey::WalletTx(*tx_id);
            let value = bincode::serialize(&tx).unwrap();
            self.put(key, value).await?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use scopeguard::defer;

    use bc_base::test_utils::test_tx;

    fn get_db_path(prefix: &str) -> std::path::PathBuf {
        bc_util::convert_to_temp_dir(prefix).path().to_path_buf()
    }

    fn create_pubkey() -> PublicKeyBytes {
        PublicKeyBytes::from_hex_str(
            "0279be667ef9dcbbac55a06295ca53cf0d28788d2f81c3908e60506fbbec30a0ab",
        )
        .unwrap()
    }

    #[tokio::test]
    async fn test_wallet_storage_new() {
        let path = get_db_path("test_bc_lib_storage");
        defer! {
            WalletStorageBase::destroy_db_in_disk(&path);
        };

        let storage_result = WalletStorageBase::new(&path);
        assert!(storage_result.is_ok());
        let _storage = storage_result.unwrap();
    }

    #[tokio::test]
    async fn test_wallet_storage_flush_database() {
        let path = get_db_path("test_bc_lib_storage_flush");
        defer! {
            WalletStorageBase::destroy_db_in_disk(&path);
        };

        let storage = WalletStorageBase::new(&path).unwrap();
        let flush_result = storage.flush_database().await;
        assert!(flush_result.is_ok());
    }

    fn prepare_wallet_data() -> WalletCache {
        let mut wallet_cache = WalletCache::new();

        let pub_key = create_pubkey();
        let priv_key = PrivateKeyBytes::from_hex_str(
            "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7ea7b09d2ff9ca603",
        )
        .unwrap();
        let pub_key_hash = wallet_cache.add_keypair(&pub_key, &priv_key).unwrap();
        wallet_cache
            .add_account_name(&pub_key_hash, "test_account")
            .unwrap();
        wallet_cache.set_default_account_(&pub_key_hash).unwrap();

        let tx = test_tx::new_tx_simple(100);
        let tx_id = tx.calc_tx_id();
        wallet_cache.add_tx_to_wallet(&tx_id, &tx);

        wallet_cache
    }

    #[tokio::test]
    async fn test_load_store_wallet() {
        let path = get_db_path("test_load_store_wallet");
        defer! {
            WalletStorageBase::destroy_db_in_disk(&path);
        };

        let storage = WalletStorageBase::new(&path).unwrap();
        let wallet_cache = prepare_wallet_data();

        storage.store_wallet(&wallet_cache).await.unwrap();

        let wallet_in_mem_new = WalletCache::new();
        let wallet_in_mem_new = storage.load_to_wallet_cache(wallet_in_mem_new).await;

        assert_eq!(wallet_cache, wallet_in_mem_new);
    }
}
